import math, random

class Player:
    #Classes
    #a class containing a record of enemy moves in 1 round
    class RoundHistory:
        def __init__(self, list_reputations, list_decisions, list_earnings):
            self.HuntHistory = [0 for x in range(20)]#HuntHistory[0] --> for >= 0 and < 0.05
            self.SlackHistory = [0 for x in range(20)]
            self.Valid = 1
            if len(list_reputations) != len(list_decisions) or len(list_decisions) != len(list_earnings):
                self.Valid = 0
            else:
                #COMPUTING ROUND HISTORY
                for x in range(len(list_earnings)):
                    if list_reputations[x] == 1 or list_reputations[x] == 0:
                        pass #DO NOT KEEP RECORDS FOR ABSOLUTE HUNTERS AND SLACKERS
                    else:
                        self.UpdateHistory(int(math.floor(list_reputations[x]*20)), list_decisions[x], list_earnings[x])
            if (sum(self.HuntHistory) + sum(self.SlackHistory)) == 0: #if there is no value, no point taking this round into account...
                self.Valid = 0

        def UpdateHistory(self, stratum, decision, earning):
            if decision == 'h':
                if earning == 0:
                    self.HuntHistory[stratum] += 1
                elif earning == -3:
                    self.SlackHistory[stratum] += 1
            elif decision == 's':
                if earning == 1:
                    self.HuntHistory[stratum] += 1
                elif earning == -2:
                    self.SlackHistory[stratum] += 1

        def GetHistory(self, action, rep):
            try:
                rep = float(rep)
            except ValueError:
                return 0
            if rep > 1 or rep < 0:
                return 0
            if rep == 1 or rep == 0: #WE DO NOT KEEP RECORDS FOR FULL SLACKERS OR FULL HUNTERS. WE KNOW THAT THEY ALWAYS SLACK AND HUNT.
                return 0
            else:
                strata = int(math.floor(rep*20))
            if action == 'h':
                return self.HuntHistory[strata]
            elif action == 's':
                return self.SlackHistory[strata]

    #Methods
    def __init__(self):
        self.TotalHunts = 0
        self.TotalSlacks = 0
        self.SuspectingOstracization = 0 #Cap is 4
        self.Mode = 'ROUND1'
        self.Aim_Type = 'CONSTANT-' #Default, 
        self.Reputation_Aim = 0.8
        self.History = []
        self.RepHistory = []
        #temporary variables
        self.temp_huntpartnerrep = []
        self.temp_decisions = []
        self.temp_lastroundnohunts = 0 #number of times people hunt in the last round, excluding our hunts
        #Information variables and user defined constants
        self.round_number = 0
        self.curr_rep = 0
        self.HunterQuantity = -1
        self.InitialHunterQuantity = -1
        self.PredictionRoundNumber = 1000 #when will the game end? On round number...?

    def GetHistory(self, action, rep, numrecords=-1): #numrecords reflects how many latest records are going to be read. <=0 means it reads all.
        sum = 0
        if numrecords <= 0:
            _HistoryQuery = self.History
        else:
            _HistoryQuery = self.History[-1*int(numrecords):] 
        for history in _HistoryQuery:
            if history.Valid: #by right this is not needed haha
                sum += history.GetHistory(action, rep)
        return sum

    def ComputeRUV(self):#Reputation Utility Value
        #Constants
        RUV_ReputationBuffer = 0.03 #The constant which defines the boundary of "acceptable reputation difference"
        RUV_NeedFactor = 2.5 #How fast the RUV will increase as the difference in reputations increase
        #print (RUV_NeedFactor**((self.Reputation_Aim-self.curr_rep)/RUV_ReputationBuffer))-1
        return (RUV_NeedFactor**((self.Reputation_Aim-self.curr_rep)/RUV_ReputationBuffer))-1

    def ComputeHuntingProbability(self, rep): #returns a float within the range [0-1]
        #Constants
        Utility_HuntHunt = 2
        Utility_HuntSlack = -1
        Utility_SlackHunt = 3
        Utility_SlackSlack = 0
        Initial_RepAim = 0.8

        if self.Mode == 'ROUND1':
            return Initial_RepAim #Round 1 we hunt with an aim to get around 0.8 rep (and get as much initial advantage as possible)
        else:
            #x is our prediction for the probability opposing player hunts with us
            if self.Mode == 'INITIALIZATION':
                #In this mode, we do not take into account any records. Assume rep is the chance the opposing player will hunt with us.
                x = rep 
            elif self.Mode == 'DEFAULT' or self.Mode == 'SURVIVAL':
                #In this mode, we take into account statistical records from the last 50 rounds. If statistical records are low (below 10 data), we will resort to rep.
                _h = self.GetHistory('h',rep,50)
                _s = self.GetHistory('s',rep,50)
                if _h+_s < 10:
                    x = rep #If statistical records are low (below 10 data), we will resort to rep.
                    #debug
                    #print('lol')
                    #raw_input("not enuf data")
                else:
                    x = float(_h)/(_h+_s)
                    #print x, rep
            
            utility_hunt = (x*(Utility_HuntHunt+self.ComputeRUV())) + ((1-x)*Utility_HuntSlack)
            utility_slack = (x*Utility_SlackHunt) + ((1-x)*Utility_SlackSlack)
            if (utility_hunt < 0):
                if utility_slack > 0:
                    return 0
                else:
                    #return 1-(utility_hunt/(utility_hunt+utility_slack)) #MAY BE ERRATIC. but for all I know this will never happen.
                    return 0
            if utility_slack < 0:
                utility_slack = 0
            #print utility_hunt, utility_slack, 'for', rep
            return utility_hunt/(utility_hunt+utility_slack)
        return 0.5 #In case there is error, just randomize XD

    def UpdateMode(self, round_number, curr_food):
        if self.Mode == 'ROUND1' and round_number != 1: #switch mode after round 1 because all players' rep will be seen
            self.Mode = 'INITIALIZATION'
        elif self.Mode == 'INITIALIZATION' and round_number > 10:
            self.Mode = 'DEFAULT'
            self.Aim_Type = 'CONSTANT0'
        #if food drops below half of original value
        elif self.Mode != 'SURVIVAL' and curr_food < (self.InitialHunterQuantity-1)*150 and round_number < self.PredictionRoundNumber/2:
            self.Mode = 'SURVIVAL'
            #raw_input('survival more impt yo')

    def UpdateAim(self, player_reputations):
        #Constants
        Buffer_Average = 0.5
        Buffer_Median = 0.25

        if self.Aim_Type.find('AVERAGE') != -1: #if Aim_Type contains average
            ave = sum(player_reputations)/len(player_reputations)
            if self.Aim_Type == 'ABOVE AVERAGE':
                ave_buffer = Buffer_Average*(max(player_reputations)-ave)
                self.Reputation_Aim = ave + ave_buffer
            elif self.Aim_Type == 'AVERAGE':
                self.Reputation_Aim = ave
            elif self.Aim_Type == 'BELOW AVERAGE':
                ave_buffer = Buffer_Average*(ave-min(player_reputations))
                self.Reputation_Aim = ave - ave_buffer
        elif self.Aim_Type.find('MEDIAN') != -1: #if Aim_Type contains median
            median_index = int(math.floor(len(player_reputations)/2))
            median_buffer = int(math.floor(len(player_reputations)*Buffer_Median))
            player_reputations.sort()
            if self.Aim_Type == 'ABOVE MEDIAN':
                self.Reputation_Aim = player_reputations[median_index+median_buffer]
            elif self.Aim_Type == 'MEDIAN':
                self.Reputation_Aim = player_reputations[median_index]
            elif self.Aim_Type == 'BELOW MEDIAN':
                self.Reputation_Aim = player_reputations[median_index-median_buffer]

    def ChangeAim(self):
        #Constants
        priority_list = ['CONSTANT0','ABOVE AVERAGE','BELOW AVERAGE', 'CONSTANT2', 'ABOVE MEDIAN', 'CONSTANT3', 'BELOW MEDIAN', 'CONSTANT4', 'MEDIAN', 'CONSTANT5', 'AVERAGE']
        priority_list.append(priority_list[0]) #make sure it works in a loop
        Constant_UpperRange = 0.9
        Constant_Center = 0.78
        Constant_LowerRange = 0.18
        if self.curr_rep < 0.22: #In extreme cases use
            Constant_LowerRange = 0.00

        changeaim_flag = 0 #flag
        for i in range(len(priority_list)-1): #-1 due to the lazy looping method. Check constants.
            if priority_list[i] == self.Aim_Type and changeaim_flag == 0:
                changeaim_flag = 1
                self.Aim_Type = priority_list[i+1]
                if self.round_number < 100:
                    self.History = [] #Reset History when you change aim if rep still not so stable
                #debug
                #raw_input("FUCK THIS SHIT I GO CHANGE AIM TO " + self.Aim_Type)
        if changeaim_flag == 0:
            self.Aim_Type = priority_list[0] #something wrong. Reset condition...

        if self.Aim_Type[:-1] == 'CONSTANT': #Constant Number Generator
            _r = random.random()-0.5
            if _r>0:
                self.Reputation_Aim = Constant_Center + ((Constant_UpperRange-Constant_Center)*(_r/0.5))
            else:
                self.Reputation_Aim = Constant_Center + ((Constant_Center-Constant_LowerRange)*(_r/0.5))

    def Is_Reputation_Stable(self):
        #Constants
        Buffer = 0.03
        NoOfRoundsConsidered = 8
        if len(self.History) >= NoOfRoundsConsidered: #At least 8 rounds...
            if (max(self.RepHistory[-NoOfRoundsConsidered:]) - min(self.RepHistory[-NoOfRoundsConsidered:]) < Buffer):
                return True
            else:
                return False

    # All the other functions are the same as with the non object oriented setting (but they
    # should be instance methods so don't forget to add 'self' as an extra first argument).

    def hunt_choices(self, round_number, current_food, current_reputation, m, player_reputations):
        #Updates
        self.UpdateMode(round_number, current_food)
        self.UpdateAim(player_reputations)
        self.temp_huntpartnerrep = list(player_reputations)
        self.round_number = round_number
        self.curr_rep = current_reputation #UPDATE THIS SO THIS WILL CHANGE EVERY SINGLE HUNT...
        self.RepHistory.append(current_reputation)
        self.HunterQuantity = len(player_reputations)+1
        if (self.InitialHunterQuantity == -1):
            self.InitialHunterQuantity = self.HunterQuantity+1

        hunt_decisions = []
        for player in player_reputations:
            _data_is_uptodate = False
            if self.round_number != 1:
                #print self.curr_rep, self.TotalHunts/(self.TotalHunts+self.TotalSlacks)
                if float(self.TotalHunts)/(self.TotalHunts+self.TotalSlacks) == self.curr_rep:
                    _data_is_uptodate = True
            else:
                _data_is_uptodate = True
            if (random.random() < self.ComputeHuntingProbability(player)): #if HuntingProbability = 0, 100% slack is assured. if =1, 100% hunt is assured.
                hunt_decisions.append('h')
                self.TotalHunts += 1
            else:
                hunt_decisions.append('s')
                self.TotalSlacks += 1
            if _data_is_uptodate: #once data is not up to date, it is not used.
                self.curr_rep = float(self.TotalHunts)/(self.TotalHunts+self.TotalSlacks)
        #print hunt_decisions
        #PRIORITY IMPLEMENTATION----------------------------------------------------------- This makes your decisions more fixed and reliable.
        #---------------------------------- However, this makes higher ranking players more likely to win. (coz you will never slack against them)
        if self.Mode == 'SURVIVAL':
            _uncooperative_hunters = 0 #number of hunters who have 0 chance of working with you
            _huntprioritylist = []
            for i in range(len(player_reputations)):
                _item = (self.ComputeHuntingProbability(player_reputations[i]), i)
                if _item[0] <= 0:
                    _uncooperative_hunters += 1
                _huntprioritylist.append(_item)
            _huntprioritylist.sort(reverse=True)
            #print _huntprioritylist
            _new_hunt_decisions = ['s' for i in range(len(hunt_decisions))]
            _hunts = hunt_decisions.count('h')
            #CAN WE GET THE COOPERATION TRESHOLD?
            if self.temp_lastroundnohunts*0.9 + _hunts < m: #predicted that cooperation treshold will not be reached
                _xtrahuntneeded = int(math.floor(m-(self.temp_lastroundnohunts*0.9)))
                #print 'xtra hunt needed:', _xtrahuntneeded, len(_huntprioritylist), _uncooperative_hunters
                _maxhuntgiven = len(_huntprioritylist)-_uncooperative_hunters
                if _maxhuntgiven >= _xtrahuntneeded: #if we can make a difference
                    _hunts = _xtrahuntneeded #make that difference
                    #raw_input('going for m')
            for i in range(_hunts):
                _new_hunt_decisions[int(_huntprioritylist[i][1])] = 'h'
            hunt_decisions = _new_hunt_decisions
        #END OF NEW IMPLEMENTATION--------------------------------------------------------------------------
        #STORING TEMP VARIABLES FOR hunt_outcomes and round_end
        self.temp_decisions = list(hunt_decisions) #copy list
        #print hunt_decisions
        #print player_reputations
        return hunt_decisions

    def hunt_outcomes(self, food_earnings):
        _RoundHistory = self.RoundHistory(self.temp_huntpartnerrep, self.temp_decisions, food_earnings)
        if _RoundHistory.Valid == True: #Only add this to history when it is valid...
            self.History.append(_RoundHistory)

    def round_end(self, award, m, number_hunters):
        #update values
        self.temp_lastroundnohunts = number_hunters - self.temp_decisions.count('h') #excludes our own hunts
        #print self.temp_lastroundnohunts, number_hunters
        #Check for ostracization
        #Constants
        SuspectingOstracization_Buffer = 0.8 #% of hunts that should we get, 'at least'...
        SuspectingOstracization_Treshold = 5
        if (self.Aim_Type == 'CONSTANT0' and self.Reputation_Aim == 0.8): #OUR DEFAULT AIM SHOULD HAVE A HIGHER TRESHOLD
            SuspectingOstracization_Treshold = 7
        if self.Is_Reputation_Stable(): #If our rep is more or less stable...
            #print self.Reputation_Aim, self.curr_rep, self.SuspectingOstracization
            #if sum(self.History[-1].HuntHistory)<((number_hunters/self.HunterQuantity)*SuspectingOstracization_Buffer): #but we are treated unfairly
            if sum(self.History[-1].HuntHistory) < self.temp_decisions.count('h')*SuspectingOstracization_Buffer:
                self.SuspectingOstracization += 1
                if self.SuspectingOstracization >= SuspectingOstracization_Treshold:
                    self.SuspectingOstracization = 0
                    self.ChangeAim() #CHANGE AIM!
                #debug
                if (self.SuspectingOstracization >= SuspectingOstracization_Treshold-1):
                    #raw_input("WHAT THE F")
                    pass
            else:
                self.SuspectingOstracization = 0 #Once we are treated fairly, we forget all evil past